const std = @import("std");
const os = @import("../os.zig");
const fs = @import("../fs.zig");
const Inode = fs.Inode;
const Dirent = Inode.Dirent;
const Name = Dirent.Name;

/// Looks up and returns the inode for the given path.
/// The caller must call put() to release the reference when done.
/// Returns an error if the path is invalid or any component is not found.
fn path2inode(path: []const u8) !*Inode {
    var parent: *Inode = if (path.len > 0 and path[0] == '/')
        .get(fs.ROOTINO)
    else
        os.proc.current().?.cwd.dup();

    const PathIterator = std.fs.path.ComponentIterator(.posix, u8);
    var it: PathIterator = .init(path);

    var next: *Inode = parent;
    while (it.next()) |comp| : (parent = next) {
        defer parent.put();

        parent.lock();
        defer parent.unlock();

        if (parent.disk.type != .dir) {
            return error.NotDir;
        }

        const child_num, _ = parent.childInode(.init(comp.name)) orelse return error.FileNotFound;
        next = .get(child_num);
    }

    return next;
}

/// Looks up and returns the inode for the given path.
/// The caller must call put() to release the reference when done.
pub fn getinode(path: [*:0]const u8) !*Inode {
    if (path[0] == 0) return error.BadPathName;
    return path2inode(std.mem.span(path));
}

/// Returns the parent inode and child name for the given path.
/// The parent inode is referenced; caller must call put() when done.
pub fn getparent(path: [*:0]const u8) !struct { *Inode, Name } {
    if (path[0] == 0) return error.BadPathName;
    const full_path = std.mem.span(path);
    const parent = std.fs.path.dirnamePosix(full_path) orelse n: {
        break :n if (path[0] == '/') path[0..1] else "";
    };

    const inode = try path2inode(parent);
    errdefer inode.put();

    inode.lock();
    const is_dir = inode.disk.type == .dir;
    inode.unlock();

    if (is_dir) {
        const basename = std.fs.path.basenamePosix(full_path);
        return .{ inode, .init(basename) };
    } else {
        return error.NotDir;
    }
}

/// Creates a new inode at the given path and returns it locked.
/// Caller must unlock the inode and drop the reference when done.
pub fn create(path: [*:0]const u8, file_type: Inode.Type, major: u16, minor: u16) !*Inode {
    const dir, const name = try getparent(path);
    defer dir.put();

    dir.lock();
    defer dir.unlock();

    if (dir.childInode(name)) |child| {
        const ino = Inode.get(child[0]);
        errdefer ino.put();

        ino.lock();
        errdefer ino.unlock();

        if (file_type == .file and switch (ino.disk.type) {
            .file, .device, .symlink => true,
            else => false,
        }) return ino;

        return error.IsDir;
    }

    const inode = try Inode.alloc(file_type);
    errdefer inode.put();
    inode.lock();
    errdefer inode.unlock();

    // Create . and .. entries.
    if (file_type == .dir) {
        for (Dirent.dot_dirs, [2]u32{ inode.inum, dir.inum }) |dot_dir, inum| {
            try inode.addEntry(dot_dir, inum);
        }
    }
    try dir.addEntry(name, inode.inum);

    // asserts that the following code should never fail
    errdefer comptime unreachable;

    if (file_type == .dir) {
        dir.disk.nlink += 1; // for ".."
        dir.flush();
    }

    inode.disk.major = major;
    inode.disk.minor = minor;
    inode.disk.nlink = 1;
    inode.flush();

    return inode;
}
